package com.zhuhjay.service.system.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhuhjay.common.base.config.SystemConfig;
import com.zhuhjay.common.base.enums.HttpCode;
import com.zhuhjay.common.base.exception.BasicBusinessException;
import com.zhuhjay.common.base.exception.ServerErrorException;
import com.zhuhjay.common.base.util.BeanBuilder;
import com.zhuhjay.common.base.util.BigDecimalUtils;
import com.zhuhjay.common.redis.util.RedisUtils;
import com.zhuhjay.common.security.context.LoginUserContext;
import com.zhuhjay.common.web.model.vo.PageInfoVo;
import com.zhuhjay.common.web.util.FileUtils;
import com.zhuhjay.service.system.mapper.SystemFileMapper;
import com.zhuhjay.service.system.model.domain.FileCounter;
import com.zhuhjay.service.system.model.domain.FileInfo;
import com.zhuhjay.service.system.model.domain.FolderInfo;
import com.zhuhjay.service.system.model.dto.FileChunkDto;
import com.zhuhjay.service.system.model.dto.MdChunkDto;
import com.zhuhjay.service.system.model.entity.SystemDir;
import com.zhuhjay.service.system.model.entity.SystemFile;
import com.zhuhjay.service.system.model.entity.SystemUser;
import com.zhuhjay.service.system.model.vo.CheckChunkVo;
import com.zhuhjay.service.system.model.vo.DirInfoVo;
import com.zhuhjay.service.system.model.vo.FilesCounterInfoVo;
import com.zhuhjay.service.system.model.vo.MdFileInfoVo;
import com.zhuhjay.service.system.service.SystemConfigService;
import com.zhuhjay.service.system.service.SystemDirService;
import com.zhuhjay.service.system.service.SystemFileService;
import com.zhuhjay.service.system.service.SystemUserService;
import com.zhuhjay.service.system.util.LogUtil;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.RandomAccessFile;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.stream.Collectors;

import static com.zhuhjay.common.base.constant.ApplicationConstant.*;
import static com.zhuhjay.common.base.constant.CacheConstant.FILE_CAN_UPLOAD_PREFIX;
import static com.zhuhjay.common.base.constant.CacheConstant.FILE_UPLOAD_PREFIX;
import static com.zhuhjay.common.base.constant.DataConstant.MB;
import static com.zhuhjay.common.base.constant.TimeConstant.MINUTE;
import static com.zhuhjay.service.system.enums.DirPowerEnum.COMMON;
import static com.zhuhjay.service.system.enums.DirPowerEnum.PRIVATE;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author Zhuhjay
 * @since 2023-02-07
 */
@Service
public class SystemFileServiceImpl extends ServiceImpl<SystemFileMapper, SystemFile> implements SystemFileService {

    @Resource private SystemConfig systemConfig;
    @Lazy @Resource private SystemDirService dirService;
    @Resource private SystemUserService userService;
    @Resource private SystemConfigService configService;
    @Resource private ExecutorService executorService;

    @Override
    public CheckChunkVo check(String dirId, FileChunkDto chunkInfo) {
        // COMMON 目录不可直接存放文件
        checkFile2CommonDir(dirId);

        // 检查文件夹是否存在
        Integer userId = LoginUserContext.getUserId();
        checkDirExist(userId, dirId);

        // 检查文件大小是否超过上限
        String maxSizeStr = configService.getConfigValue("system.file.upload.max.size");
        long maxSize = Integer.parseInt(maxSizeStr) * MB;
        if (chunkInfo.getTotalSize() > maxSize) {
            throw new BasicBusinessException(StrUtil.format("文件大小超过上限({}MB)", maxSizeStr));
        }

        String identifier = chunkInfo.getIdentifier();
        Long count = this.lambdaQuery()
                .eq(SystemFile::getIdentifier, identifier)
                .count();
        // 文件已上传
        if (count != null && count > 0) {
            // 当前用户的文件夹下已存在该文件, 不存在则创建一份数据于当前目录下
            SystemFile systemFile = this.lambdaQuery()
                    .eq(SystemFile::getIdentifier, identifier).eq(SystemFile::getDirId, dirId)
                    .one();
            if (systemFile == null) {
                saveFileChunkInfoToDb(chunkInfo, dirId);
            }
            return BeanBuilder.builder(CheckChunkVo::new)
                    .with(CheckChunkVo::setUploaded, true)
                    .with(CheckChunkVo::setExisted, systemFile != null)
                    .build();
        }
        // 查看是否存在未上传完的文件
        String key = FILE_UPLOAD_PREFIX + identifier;
        Set<String> members = RedisUtils.getMembers4Set(key);
        List<Integer> chunks = members == null || members.isEmpty() ? null :
                members.stream().map(Integer::parseInt).collect(Collectors.toList());
        // 标记该文件可进行上传
        RedisUtils.setCacheAndExpire(FILE_CAN_UPLOAD_PREFIX + identifier, 1, 5 * MINUTE);
        // 文件 未上传完 / 未上传
        return BeanBuilder.builder(CheckChunkVo::new)
                .with(CheckChunkVo::setUploaded, false)
                .with(CheckChunkVo::setUploadedChunks, chunks)
                .build();
    }

    @Override
    public void fileUpload(String dirId, FileChunkDto chunkInfo) {
        // COMMON 目录不可直接存放文件
        checkFile2CommonDir(dirId);

        // 检查文件夹是否存在
        Integer userId = LoginUserContext.getUserId();
        checkDirExist(userId, dirId);

        MultipartFile multipartFile = chunkInfo.getFile();
        if (multipartFile == null) {
            throw new BasicBusinessException("未上传文件");
        }

        // 检测是否存在可上传的标记
        String identifier = chunkInfo.getIdentifier();
        if (!RedisUtils.hasKey(FILE_CAN_UPLOAD_PREFIX + identifier)) {
            throw new BasicBusinessException("文件未被许可上传");
        }

        final String realPath = systemConfig.getFileSystemPath();
        File dir = FileUtil.mkdir(realPath);
        String fileName = dir + "/" + identifier;
        if (chunkInfo.getTotalChunks() == 1) {
            // 单文件上传
            singletonUploadFile(fileName, chunkInfo, dirId);
        } else {
            // 分片上传
            shardingUploadFile(fileName, chunkInfo, dirId);
        }
    }

    @Override
    @Transactional(rollbackFor = BasicBusinessException.class)
    public void copy(List<String> fileIds, String dirId) {
        // COMMON 目录不可直接存放文件
        checkFile2CommonDir(dirId);

        List<SystemFile> files = new ArrayList<>(fileIds.size());
        Integer userId = LoginUserContext.getUserId();
        fileIds.forEach(fileId -> {
            // 检查文件夹、文件是否存在、是否达成可拷贝条件, 返回对应的文件
            SystemFile file = getFileIfTargetFolderNotExist(fileId, dirId);

            // 复制一份信息到该用户的文件列表中
            SystemFile copy = BeanBuilder.builder(SystemFile::new)
                    .with(SystemFile::setRealName, file.getRealName())
                    .with(SystemFile::setIdentifier, file.getIdentifier())
                    .with(SystemFile::setType, file.getType())
                    .with(SystemFile::setSize, file.getSize())
                    .with(SystemFile::setDirId, dirId)
                    .with(SystemFile::setUserId, userId)
                    .build();
            files.add(copy);
        });
        // 符合条件, 批量保存
        this.saveBatch(files);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void move(List<String> fileIds, String dirId) {
        // COMMON 目录不可直接存放文件
        checkFile2CommonDir(dirId);

        Integer userId = LoginUserContext.getUserId();
        fileIds.forEach(fileId -> {
            // 检查文件夹、文件是否存在、是否达成可移动条件
            getFileIfTargetFolderNotExist(fileId, dirId);

            // 文件所有者是否是当前用户
            Long count = this.lambdaQuery()
                    .eq(SystemFile::getId, fileId)
                    .eq(SystemFile::getUserId, userId)
                    .eq(SystemFile::getStatus, 1)
                    .count();
            if (count == null || count == 0) {
                throw new BasicBusinessException("无权限操作");
            }
        });
        // 都符合条件, 批量移动
        this.lambdaUpdate().in(SystemFile::getId, fileIds)
                .set(SystemFile::getDirId, dirId).update();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void delete2Recycle(List<String> fileIds) {
        Integer userId = LoginUserContext.getUserId();
        // 如果是超级管理员, 则不需要判断
        if (!SUPER_ADMIN_ID.equals(userId)) {
            // 判断是否是本人操作
            Long count = this.lambdaQuery()
                    .in(SystemFile::getId, fileIds)
                    .eq(SystemFile::getUserId, userId)
                    .eq(SystemFile::getStatus, 1)
                    .count();
            // 文件数对不上, 说明有文件不存在或者不是本人操作
            if (count == null || count.intValue() != fileIds.size()) {
                throw new BasicBusinessException("删除错误, 文件不存在或者无权限操作");
            }
        }
        this.lambdaUpdate().in(SystemFile::getId, fileIds)
                .set(SystemFile::getStatus, 0).update();
    }

    @Override
    public void rename(String fileId, String name) {
        // 检查文件是否存在
        SystemFile file = this.lambdaQuery()
                .eq(SystemFile::getId, fileId).eq(SystemFile::getStatus, 1)
                .oneOpt().orElseThrow(() -> new BasicBusinessException("文件不存在"));

        // 检查当前目录下是否存在同名文件
        String dirId = file.getDirId();
        Long count = this.lambdaQuery().eq(SystemFile::getDirId, dirId)
                .eq(SystemFile::getRealName, name).count();
        if (count != null && count > 0) {
            throw new BasicBusinessException("当前目录下已存在同名文件(或存在于回收站)");
        }

        // 重命名文件
        this.lambdaUpdate().eq(SystemFile::getId, fileId)
                .set(SystemFile::getRealName, name).update();
    }

    @Override
    public void downloadFile(String id, HttpServletRequest request, HttpServletResponse response) {
        // 通过附件模式下载文件, 用作下载
        loadFile(id, "attachment", request, response);
    }

    @Override
    public void previewFile(String id, HttpServletRequest request, HttpServletResponse response) {
        // 通过内联模式下载文件, 用作预览
        loadFile(id, "inline", request, response);
    }

    /**
     * 加载文件
     *
     * @param id       文件id
     * @param mode     下载模式(预览/下载)
     * @param request  请求
     * @param response 响应
     */
    private void loadFile(String id, String mode, HttpServletRequest request, HttpServletResponse response) {
        SystemFile systemFile = this.lambdaQuery()
                .eq(SystemFile::getId, id).eq(SystemFile::getStatus, 1)
                .oneOpt().orElseThrow(() -> new BasicBusinessException("文件不存在"));
        // 获取文件系统路径
        String realPath = systemConfig.getFileSystemPath();
        // 拼接文件路径
        String filePath = realPath + PATH_SEPARATOR + systemFile.getIdentifier();
        // 进行文件下载
        File file = new File(filePath);
        final String fileName = systemFile.getRealName();
        BufferedInputStream in = null;
        try {
            in = FileUtil.getInputStream(file);
            response.setHeader("Content-Disposition", StrUtil.format("{};filename=\"{}\"",
                    mode, FileUtils.encodeFileName(request, fileName)));
            // 设置响应头, 告知浏览器文件的大小
            response.setContentLengthLong(file.length());
            response.setContentType(systemFile.getType());
            ServletUtil.write(response, in);
            // 只记录下载成功日志
            if ("attachment".equals(mode)) {
                LogUtil.record(
                        "CommonController.download", "文件下载(FileSystem)", StrUtil.format("下载文件[{}]", fileName),
                        null, HttpCode.SUCCESS, 0);
            }
        } catch (Exception e) {
            String methodName = StrUtil.format("CommonController.{}", "attachment".equals(mode)? "download" : "preview");
            // 记录失败日志
            LogUtil.record(
                    methodName, "文件传输失败记录(FileSystem)", StrUtil.format("传输文件[{}]", fileName),
                    e.getMessage(), HttpCode.SERVER_ERROR, 0);
        } finally {
            IoUtil.close(in);
        }
    }

    @Override
    public FilesCounterInfoVo counter() {
        FilesCounterInfoVo vo = new FilesCounterInfoVo();

        // 使用线程池来执行任务
        Future<List<FileCounter>> futureCounter = executorService.submit(() -> {
            // 查询所有用户
            List<SystemUser> users = userService.list();
            return users.stream().map(user -> {
                FileCounter counterVo = new FileCounter();
                // 统计用户的文件数
                QueryWrapper<SystemFile> fileQueryWrapper = new QueryWrapper<>();
                fileQueryWrapper.select("count(*) as count, sum(size) as size")
                        .eq("user_id", user.getId());
                Map<String, Object> fileMap = this.getMap(fileQueryWrapper);

                counterVo.setUsername(user.getUsername());
                long count = (Long) fileMap.get("count");
                double size = BigDecimalUtils.byte2Mb((BigDecimal) fileMap.get("size"));
                counterVo.setFileCount(count);
                counterVo.setTakeUpSpace(size);
                // 计算系统统计
                vo.addFileCount(count);
                vo.addTakeUpSpace(size);

                // 统计文件夹数
                QueryWrapper<SystemDir> dirQueryWrapper = new QueryWrapper<>();
                dirQueryWrapper.eq("user_id", user.getId());
                long dirCount = dirService.getBaseMapper().selectCount(dirQueryWrapper);
                counterVo.setFolderCount(dirCount);
                return counterVo;
            }).collect(Collectors.toList());
        });

        Future<Boolean> future = executorService.submit(() -> {
            // 统计unique文件数
            long uniqueFileCount = this.baseMapper.selectCount(
                    new QueryWrapper<SystemFile>().select("distinct identifier"));
            vo.setUniqueFileCount(uniqueFileCount);
            // 访问本地文件夹, 统计文件个数以及占用空间
            File dir = new File(systemConfig.getFileSystemPath());
            FileUtil.walkFiles(dir, file -> {
                vo.increaseRealFileCount();
                vo.addRealTakeUpSpace(BigDecimalUtils.byte2Mb(file.length()));
            });
            return true;
        });

        try {
            vo.setFileCounters(futureCounter.get());
            future.get();
        } catch (Throwable e) {
            throw new ServerErrorException("统计文件信息失败", e);
        }
        return vo;
    }

    @Override
    public MdFileInfoVo markdownUpload(String dirId, MdChunkDto chunkInfo) {
        // COMMON 目录不可直接存放文件
        checkFile2CommonDir(dirId);

        // 检查文件夹是否存在
        Integer userId = LoginUserContext.getUserId();
        checkDirExist(userId, dirId);

        String fileId = chunkInfo.getId();
        String identifier = chunkInfo.getIdentifier();
        SystemFile file;

        // 判断该文件是否存在, 构建数据库存储对象 TODO: 对于Markdown需要再考虑一下
        if (StrUtil.isBlank(fileId)) {
            // 是 save 操作的情况下, 判断文件是否已存在(防止恶意)
            this.lambdaQuery()
                    .eq(SystemFile::getIdentifier, identifier)
                    .eq(SystemFile::getDirId, dirId)
                    .eq(SystemFile::getUserId, userId)
                    .oneOpt().ifPresent(f -> {
                        throw new BasicBusinessException("保存失败, 文件已存在(或存在于回收站)");
                    });
            // 构建新对象
            file = BeanBuilder.builder(SystemFile::new)
                    .with(SystemFile::setType, MediaType.APPLICATION_OCTET_STREAM_VALUE)
                    .with(SystemFile::setUserId, userId)
                    .with(SystemFile::setDirId, dirId).build();
        } else {
            // 是 update 操作的情况下, 获取文件信息
            file = this.lambdaQuery()
                    .eq(SystemFile::getId, fileId)
                    .eq(SystemFile::getDirId, dirId)
                    .eq(SystemFile::getUserId, userId)
                    .oneOpt().orElseThrow(() -> new BasicBusinessException("更新失败, 文件不存在"));
            // 如果发现 identifier 一致, 则认为并没有修改文件内容
            if (identifier.equals(file.getIdentifier())) {
                // 如果文件名不一致, 则更新文件名即可
                if (!chunkInfo.getName().equals(file.getRealName())) {
                    this.lambdaUpdate().set(SystemFile::getRealName, chunkInfo.getName())
                            .eq(SystemFile::getId, fileId).update();
                }
                return null;
            }
        }
        // 更新一些信息
        byte[] bytes = chunkInfo.getContent().getBytes(StandardCharsets.UTF_8);
        file.setIdentifier(identifier);
        file.setSize(Integer.toUnsignedLong(bytes.length));
        file.setRealName(chunkInfo.getName());

        final String realPath = systemConfig.getFileSystemPath();
        File dir = FileUtil.mkdir(realPath);
        String fileName = dir + "/" + identifier;

        try {
            FileUtil.writeBytes(bytes, fileName);
        } catch (IORuntimeException e) {
            throw new BasicBusinessException("文件保存失败");
        }

        // 判断数据库保存方案
        if (StrUtil.isBlank(fileId)) {
            this.save(file);
            return BeanBuilder.builder(MdFileInfoVo::new)
                    .with(MdFileInfoVo::setId, file.getId())
                    .with(MdFileInfoVo::setDirId, file.getDirId())
                    .build();
        } else {
            this.updateById(file);
        }
        return null;
    }

    @Override
    public MdFileInfoVo markdown(String fileId) {
        SystemFile file = this.lambdaQuery()
                .eq(SystemFile::getId, fileId)
                .eq(SystemFile::getStatus, 1)
                .likeLeft(SystemFile::getRealName, ".md")
                .oneOpt().orElseThrow(() -> new BasicBusinessException("文件不存在"));
        String realPath = systemConfig.getFileSystemPath();
        String filePath = realPath + PATH_SEPARATOR + file.getIdentifier();
        File markdownFile = new File(filePath);
        if (!markdownFile.exists()) {
            throw new BasicBusinessException("文件不存在");
        }
        String content;
        try {
            content = FileUtil.readString(markdownFile, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new BasicBusinessException("文件读取失败");
        }
        // 根据是否登录, 返回不同的内容
        String username = LoginUserContext.getUsername();
        if (StrUtil.isBlank(username)) {
            // 没登陆的情况下, 只返回文件内容
            return BeanBuilder.builder(MdFileInfoVo::new)
                    .with(MdFileInfoVo::setContent, content)
                    .with(MdFileInfoVo::setName, file.getRealName())
                    .build();
        } else {
            return BeanBuilder.builder(MdFileInfoVo::new)
                    .with(MdFileInfoVo::setId, file.getId())
                    .with(MdFileInfoVo::setContent, content)
                    .with(MdFileInfoVo::setName, file.getRealName())
                    .with(MdFileInfoVo::setDirId, file.getDirId())
                    .with(MdFileInfoVo::setIdentifier, file.getIdentifier())
                    .build();
        }
    }

    @Override
    public PageInfoVo<MdFileInfoVo> markdownPage(Integer pageNum, Integer pageSize) {
        PageInfoVo<MdFileInfoVo> pageInfoVo = BeanBuilder.builder(PageInfoVo<MdFileInfoVo>::new)
                .with(PageInfoVo::setCurrent, pageNum)
                .with(PageInfoVo::setSize, pageSize)
                .build();
        // 查询 md 文件信息
        Page<SystemFile> files = this.lambdaQuery()
                .eq(SystemFile::getUserId, LoginUserContext.getUserId())
                .eq(SystemFile::getStatus, 1)
                // %.md
                .likeLeft(SystemFile::getRealName, ".md")
                .orderByAsc(SystemFile::getCreateTime)
                .page(Page.of(pageNum, pageSize));

        List<MdFileInfoVo> records = files.getRecords().stream().map(file -> {
            // 获取该文件所在的目录路径
            List<String> paths = dirService.getFolderPaths(file.getDirId()).stream()
                    .map(FolderInfo::getName).collect(Collectors.toList());
            // 拼接路径
            StringJoiner joiner = new StringJoiner(PATH_SEPARATOR);
            paths.forEach(joiner::add);
            return BeanBuilder.builder(MdFileInfoVo::new)
                    .with(MdFileInfoVo::setId, file.getId())
                    .with(MdFileInfoVo::setName, file.getRealName())
                    .with(MdFileInfoVo::setDirId, file.getDirId())
                    .with(MdFileInfoVo::setDirPath, joiner.toString())
                    .build();
        }).collect(Collectors.toList());
        pageInfoVo.setRecords(records);
        pageInfoVo.setTotal(files.getTotal());
        return pageInfoVo;
    }

    @Override
    public DirInfoVo recycleList() {
        Integer userId = LoginUserContext.getUserId();
        String username = LoginUserContext.getUsername();
        // 查询回收站文件
        List<SystemFile> files = this.lambdaQuery()
                .eq(SystemFile::getUserId, userId)
                .eq(SystemFile::getStatus, 0)
                .orderByDesc(SystemFile::getCreateTime)
                .orderByAsc(SystemFile::getRealName)
                .list();

        DirInfoVo dirInfoVo = new DirInfoVo();

        // 设置当前目录(模拟回收站)
        FolderInfo folder = BeanBuilder.builder(FolderInfo::new)
                .with(FolderInfo::setId, DigestUtil.md5Hex("回收站"))
                .with(FolderInfo::setName, "回收站")
                .with(FolderInfo::setPower, PRIVATE.getPower())
                .build();
        dirInfoVo.setFolder(folder);

        // 设置路径(只有自己本身)
        dirInfoVo.setPath(Collections.singletonList(folder));

        // 设置父文件夹
        dirInfoVo.setParentFolders(Collections.singletonList(folder));

        // 设置文件列表
        dirInfoVo.setFiles(files.stream().map(file -> {
            FileInfo fileInfo = FileInfo.copyTo(file, username);
            // 设置文件路径
            fileInfo.setFilePath(dirService.getFolderPaths(file.getDirId()).stream()
                    .map(FolderInfo::getName).collect(Collectors.joining(PATH_SEPARATOR)));
            return fileInfo;
        }).collect(Collectors.toList()));

        dirInfoVo.setFolders(Collections.emptyList());

        return dirInfoVo;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void recycleRestore(List<String> fileIds) {
        Integer userId = LoginUserContext.getUserId();
        // 判断是否是本人操作
        Long count = this.lambdaQuery()
                .in(SystemFile::getId, fileIds)
                .eq(SystemFile::getUserId, userId)
                .eq(SystemFile::getStatus, 0)
                .count();
        // 文件数对不上, 说明有文件不存在或者不是本人操作
        if (count == null || count.intValue() != fileIds.size()) {
            throw new BasicBusinessException("恢复错误, 文件不存在或者无权限操作");
        }
        // 都符合条件, 批量恢复
        this.lambdaUpdate().in(SystemFile::getId, fileIds)
                .set(SystemFile::getStatus, 1).update();
    }

    @Override
    public void recycleDelete(List<String> fileIds) {
        Integer userId = LoginUserContext.getUserId();
        // 判断是否是本人操作
        Long count = this.lambdaQuery()
                .in(SystemFile::getId, fileIds)
                .eq(SystemFile::getUserId, userId)
                .eq(SystemFile::getStatus, 0)
                .count();
        // 文件数对不上, 说明有文件不存在或者不是本人操作
        if (count == null || count.intValue() != fileIds.size()) {
            throw new BasicBusinessException("删除错误, 文件不存在或者无权限操作");
        }
        // 都符合条件, 批量删除
        this.lambdaUpdate().in(SystemFile::getId, fileIds).remove();
    }

    /** 检查是否将文件保存到 COMMON 目录下, 告知用户该目录不可存放 */
    private void checkFile2CommonDir(String dirId) {
        if (FILE_SYSTEM_COMMON_DIR_ID.equals(dirId)) {
            throw new BasicBusinessException("该目录不可直接存放文件");
        }
    }

    /** 检查文件夹是否存在 */
    private void checkDirExist(Integer userId, String dirId) {
        // 如果文件夹是根目录则不需要检查
        if (FILE_SYSTEM_ROOT_DIR_ID.equals(dirId)) {
            return;
        }
        SystemDir dir = dirService.getById(dirId);
        if (dir == null) {
            throw new BasicBusinessException("文件夹不存在");
        }
        // 如果是共享目录(common权限)则不需要检查
        if (COMMON.getPower().equals(dir.getPower())) {
            return;
        }
        if (!dir.getUserId().equals(userId)) {
            throw new BasicBusinessException("没有操作权限");
        }
    }

    /** 文件夹、文件必须存在、目标目录不可存在该文件 */
    private SystemFile getFileIfTargetFolderNotExist(String fileId, String dirId) {
        // 检查文件夹是否存在
        Integer userId = LoginUserContext.getUserId();
        checkDirExist(userId, dirId);

        // 检查文件是否存在
        SystemFile file = this.lambdaQuery()
                .eq(SystemFile::getId, fileId).eq(SystemFile::getStatus, 1)
                .oneOpt().orElseThrow(() -> new BasicBusinessException("文件不存在"));

        // 检查目标文件夹是否存在这样一份文件
        this.lambdaQuery()
                .eq(SystemFile::getDirId, dirId)
                .eq(SystemFile::getIdentifier, file.getIdentifier())
                .oneOpt().ifPresent(f -> {
                    throw new BasicBusinessException(String.format("目标文件夹已存在[%s]文件(或存在于回收站)", file.getRealName()));
                });
        return file;
    }

    /** 单文件分片上传 */
    private void singletonUploadFile(String fileName, FileChunkDto chunkInfo, String dirId) {
        try {
            FileUtil.writeBytes(chunkInfo.getFile().getBytes(), fileName);
            saveFileChunkInfoToDb(chunkInfo, dirId);
        } catch (Exception e) {
            throw new BasicBusinessException("文件上传失败, 请重试");
        }
    }

    /** 多文件分片上传 */
    private void shardingUploadFile(String fileName, FileChunkDto chunkInfo, String dirId) {
        MultipartFile multipartFile = chunkInfo.getFile();
        try (RandomAccessFile file = new RandomAccessFile(fileName, "rw")) {
            long chunkSize = chunkInfo.getChunkSize();
            long offset = (chunkInfo.getChunkNumber() - 1) * chunkSize;
            file.seek(offset);
            file.write(multipartFile.getBytes());
        } catch (Exception e) {
            throw new BasicBusinessException("文件上传失败, 请重试");
        }

        // 保存分片信息
        String key = FILE_UPLOAD_PREFIX + chunkInfo.getIdentifier();
        RedisUtils.addValue2Set(key, String.valueOf(chunkInfo.getChunkNumber()));

        // 检查是否上传完毕
        Long count = RedisUtils.getSize4Set(key);

        // 如果分片都还没有上传齐
        if (count == null || count.intValue() != chunkInfo.getTotalChunks()) {
            return;
        }

        // 分片都上传完毕
        if (saveFileChunkInfoToDb(chunkInfo, dirId)) {
            // 删除分片记录
            RedisUtils.deleteCache(key);
        } else {
            throw new BasicBusinessException("文件上传失败, 请重试");
        }
    }

    /** 将文件信息保存到数据库中, 并删除文件可上传的标记 */
    private boolean saveFileChunkInfoToDb(FileChunkDto chunkInfo, String dirId) {
        MultipartFile multipartFile = chunkInfo.getFile();
        String name = chunkInfo.getName();
        String type = chunkInfo.getType();
        if (multipartFile != null) {
            name = multipartFile.getOriginalFilename();
            type = multipartFile.getContentType();
        }
        String identifier = chunkInfo.getIdentifier();
        boolean saved = this.save(BeanBuilder.builder(SystemFile::new)
                .with(SystemFile::setRealName, name)
                .with(SystemFile::setIdentifier, identifier)
                .with(SystemFile::setType, type)
                .with(SystemFile::setSize, chunkInfo.getTotalSize())
                .with(SystemFile::setUserId, LoginUserContext.getUserId())
                .with(SystemFile::setDirId, dirId)
                .build());
        if (saved) {
            // 删除文件可上传标记
            RedisUtils.deleteCache(FILE_CAN_UPLOAD_PREFIX + identifier);
        }
        return saved;
    }
}
